from collections.abc import Generator, Sequence

from parsimonious.exceptions import IncompleteParseError
from snuba_sdk.mql.mql import InvalidMQLQueryError, parse_mql

from sentry.models.environment import Environment
from sentry.models.project import Project
from sentry.sentry_metrics.querying.data.query import MQLQuery
from sentry.sentry_metrics.querying.errors import InvalidMetricsQueryError
from sentry.sentry_metrics.querying.types import QueryExpression, QueryOrder
from sentry.sentry_metrics.querying.visitors import (
    EnvironmentsInjectionVisitor,
    LatestReleaseTransformationVisitor,
    QueryConditionsCompositeVisitor,
    QueryValidationV2Visitor,
    VisitableQueryExpression,
)
from sentry.utils import metrics


class QueryParser:
    """
    Represents a parser which is responsible for generating queries given a list of MQLQuery(s).
    """

    def __init__(
        self,
        projects: Sequence[Project],
        environments: Sequence[Environment],
        mql_queries: Sequence[MQLQuery],
    ):
        self._projects = projects
        self._environments = environments
        self._mql_queries = mql_queries

    def _parse_mql(self, mql: str) -> VisitableQueryExpression:
        """
        Parses the field with the MQL grammar.

        Returns:
            A VisitableQueryExpression that wraps the AST generated by the query string and allows visitors to
            be applied on top.
        """
        try:
            query = parse_mql(mql)
        except InvalidMQLQueryError as e:
            metrics.incr(key="ddm.metrics_api.parsing.error")
            cause = e.__cause__
            if cause and isinstance(cause, IncompleteParseError):
                error_context = cause.text[cause.pos : cause.pos + 20]
                # We expose the entire MQL string to give more context when solving the error, since in the future we
                # expect that MQL will be directly fed into the endpoint instead of being built from the supplied
                # fields.
                raise InvalidMetricsQueryError(
                    f"The query '{mql}' could not be matched starting from '{error_context}...'"
                ) from e

            raise InvalidMetricsQueryError("The supplied query is not valid") from e

        return VisitableQueryExpression(query=query)

    def generate_queries(
        self,
    ) -> Generator[tuple[QueryExpression, QueryOrder | None, int | None], None, None]:
        """
        Generates multiple queries given a base query.

        Returns:
            A generator which can be used to obtain a query to execute and its details.
        """
        for mql_query in self._mql_queries:
            compiled_mql_query = mql_query.compile()

            query_expression = (
                self._parse_mql(compiled_mql_query.mql)
                # We validate the query.
                .add_visitor(QueryValidationV2Visitor())
                # We inject the environment filter in each timeseries.
                .add_visitor(EnvironmentsInjectionVisitor(self._environments))
                # We transform all `release:latest` filters into the actual latest releases.
                .add_visitor(
                    QueryConditionsCompositeVisitor(
                        LatestReleaseTransformationVisitor(self._projects)
                    )
                ).get()
            )
            yield query_expression, compiled_mql_query.order, compiled_mql_query.limit
